package com.hsu.yupao.api;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.gson.Gson;

import com.hsu.yupao.config.HttpSessionConfig;
import com.hsu.yupao.model.domain.Chat;
import com.hsu.yupao.model.domain.Team;
import com.hsu.yupao.model.domain.User;
import com.hsu.yupao.model.request.MessageRequest;
import com.hsu.yupao.model.vo.MessageVO;
import com.hsu.yupao.model.vo.WebSocketVO;
import com.hsu.yupao.service.ChatService;
import com.hsu.yupao.service.TeamService;
import com.hsu.yupao.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import static com.hsu.yupao.constant.ChatConstant.*;
import static com.hsu.yupao.constant.UserConstant.ADMIN_ROLE;
import static com.hsu.yupao.constant.UserConstant.USER_LOGIN_STATE;
import static com.hsu.yupao.utils.StringUtils.stringJsonListToLongSet;

/**
 * WebSocket 服务端点类，用于处理实时消息通信
 * 此类通过 WebSocket 协议提供实时的聊天功能。支持私聊、团队聊天和大厅广播等功能
 * @ClassName: WebSocket
 * @author Hsu琛君珩
 * @date 2024-06-04
 * @apiNote
 * @Version: v1.0
 */
@Component
@Slf4j
@ServerEndpoint(value = "/websocket/{userId}/{teamId}", configurator = HttpSessionConfig.class)
public class WebSocket {

    // ROOMS 存储每个团队对应的WebSocket连接，键是团队ID，值是该团队所有成员的WebSocket连接。
    private static final Map<String, ConcurrentHashMap<String, WebSocket>> ROOMS = new HashMap<>();
    // SESSIONS 是线程安全的Set，用于存储所有活跃的WebSocket会话。
    private static final CopyOnWriteArraySet<Session> SESSIONS = new CopyOnWriteArraySet<>();
    // SESSION_POOL 用于快速查找特定用户的WebSocket会话，键是用户ID。
    private static final Map<String, Session> SESSION_POOL = new HashMap<>();

    // 静态服务实例，用于在WebSocket中访问用户、聊天和团队服务。
    private static UserService userService;
    private static ChatService chatService;
    private static TeamService teamService;

    // 记录当前在线的WebSocket连接数。
    private static int onlineCount = 0;
    // 代表当前WebSocket会话的Session对象。
    private Session session;
    // HttpSession对象，用于获取当前用户的会话信息。
    private HttpSession httpSession;

    // 使用@Autowired注解来自动注入服务依赖，这里统一设置用户、聊天和团队服务。
    @Autowired
    public void setServices(UserService userService, ChatService chatService, TeamService teamService) {
        WebSocket.userService = userService;
        WebSocket.chatService = chatService;
        WebSocket.teamService = teamService;
    }

    /**
     * 当WebSocket连接打开时调用此方法。
     * @param session WebSocket会话对象，代表与某个客户端的连接会话。
     * @param userId 用户ID，从URL参数中获取。
     * @param teamId 团队ID，从URL参数中获取，用于支持团队聊天功能。
     * @param config 提供端点配置的信息，包括HTTP会话等。
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId, @PathParam(value = "teamId") String teamId, EndpointConfig config) {
        try {
            // 验证传入的userId是否有效
            if (StringUtils.isBlank(userId) || "undefined".equals(userId)) {
                sendError(userId, "参数有误");
                return;
            }
            // 从WebSocket的配置中获取HTTP会话
            HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
            User user = (User) httpSession.getAttribute(USER_LOGIN_STATE);  // 尝试获取已登录的用户
            if (user != null) {
                this.session = session;  // 保存WebSocket会话
                this.httpSession = httpSession;  // 保存HTTP会话
            }
            // 处理团队ID，为团队聊天分配WebSocket连接
            if (!"NaN".equals(teamId)) {
                if (!ROOMS.containsKey(teamId)) {
                    // 如果当前没有这个团队的聊天室，创建一个
                    ConcurrentHashMap<String, WebSocket> room = new ConcurrentHashMap<>();
                    room.put(userId, this);
                    ROOMS.put(String.valueOf(teamId), room);
                    addOnlineCount();  // 新聊天室增加在线人数
                } else {
                    // 如果已存在聊天室但当前用户未加入，则加入
                    if (!ROOMS.get(teamId).containsKey(userId)) {
                        ROOMS.get(teamId).put(userId, this);
                        addOnlineCount();  // 加入现有聊天室也增加在线人数
                    }
                }
                log.info("有新连接加入！当前在线人数为" + getOnlineCount());
            } else {
                // 对于不属于任何团队的连接，添加到全局会话集
                SESSIONS.add(session);
                SESSION_POOL.put(userId, session);
                log.info("有新用户加入，userId={}, 当前在线人数为：{}", userId, SESSION_POOL.size());
                sendAllUsers();  // 通知所有用户有新用户加入
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 当WebSocket连接关闭时调用此方法。
     * @param userId 用户ID，来自URL路径参数。
     * @param teamId 团队ID，来自URL路径参数，表示用户所在的聊天室或团队。
     * @param session 当前关闭的WebSocket会话对象。
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId, @PathParam(value = "teamId") String teamId, Session session) {
        try {
            // 检查是否为团队聊天室的连接关闭。
            if (!"NaN".equals(teamId)) {
                // 如果是团队聊天室，从对应的团队聊天室中移除用户。
                ROOMS.get(teamId).remove(userId);
                // 减少在线人数统计。
                if (getOnlineCount() > 0) {
                    subOnlineCount();
                }
                log.info("用户退出:当前在线人数为:" + getOnlineCount());
            } else {
                // 如果不是团队聊天室（即个人或大厅聊天），从SESSION_POOL中移除用户。
                if (!SESSION_POOL.isEmpty()) {
                    SESSION_POOL.remove(userId);
                    SESSIONS.remove(session);
                }
                log.info("【WebSocket消息】连接断开，总数为：" + SESSION_POOL.size());
                // 更新所有用户的在线信息。
                sendAllUsers();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 当从客户端接收到消息时，调用此方法处理WebSocket消息。
     * @param message 客户端发送的消息内容，以字符串形式表示。
     * @param userId 通过URL路径参数传入的用户ID。
     */
    @OnMessage
    public void onMessage(String message, @PathParam("userId") String userId) {
        // 检查是否为心跳检测消息
        if ("PING".equals(message)) {
            sendOneMessage(userId, "pong");
            log.error("心跳包，发送给={},在线:{}人", userId, getOnlineCount());
            return;
        }
        log.info("服务端收到用户username={}的消息:{}", userId, message);
        // 解析接收到的消息为MessageRequest对象
        MessageRequest messageRequest = new Gson().fromJson(message, MessageRequest.class);
        Long toId = messageRequest.getToId();
        Long teamId = messageRequest.getTeamId();
        String text = messageRequest.getText();
        Integer chatType = messageRequest.getChatType();
        User fromUser = userService.getById(userId);
        // 根据聊天类型分发处理
        if (chatType == PRIVATE_CHAT) {
            // 处理私聊消息
            privateChat(fromUser, toId, text, chatType);
        } else if (chatType == TEAM_CHAT) {
            // 处理团队聊天消息
            Team team = teamService.getById(teamId);
            teamChat(fromUser, text, team, chatType);
        } else {
            // 处理大厅聊天消息
            hallChat(fromUser, text, chatType);
        }
    }

    /**
     * 处理私人聊天，发送消息给指定的用户。
     * @param user     发送消息的用户。
     * @param toId     接收消息的用户ID。
     * @param text     发送的消息内容。
     * @param chatType 聊天类型（私聊）。
     */
    private void privateChat(User user, Long toId, String text, Integer chatType) {
        // 尝试从会话池中获取接收者的会话对象
        Session toSession = SESSION_POOL.get(toId.toString());
        if (toSession != null && toSession.isOpen()) {
            // 如果会话存在且开放，生成聊天结果
            MessageVO messageVO = chatService.chatResult(user.getId(), toId, text, chatType, DateUtil.date(System.currentTimeMillis()));
            // 设置消息来源是否为当前登录用户
            User loginUser = (User) this.httpSession.getAttribute(USER_LOGIN_STATE);
            if (loginUser.getId() == user.getId()) {
                messageVO.setIsMy(true);
            }
            // 将消息对象转换为JSON格式
            String toJson = new Gson().toJson(messageVO);
            // 发送消息给指定的用户会话
            sendOneMessage(toId.toString(), toJson);
            log.info("发送给用户userName={}，消息：{}", messageVO.getToUser(), toJson);
        } else {
            // 如果接收者不在线，则记录日志
            log.info("用户不在线userName={}的session", toId);
        }
        // 保存聊天记录到数据库
        saveChat(user.getId(), toId, text, null, chatType);
        chatService.deleteKey(CACHE_CHAT_PRIVATE, user.getId() + "" + toId);
        chatService.deleteKey(CACHE_CHAT_PRIVATE, toId + "" + user.getId());
    }


    /**
     * 处理团队聊天消息，将消息发送给指定团队的所有成员并保存聊天记录到数据库。
     * @param user     发送消息的用户。
     * @param text     要发送的消息文本。
     * @param team     消息所在的团队。
     * @param chatType 聊天类型，例如团队聊天。
     */
    private void teamChat(User user, String text, Team team, Integer chatType) {
        MessageVO messageVO = new MessageVO();
        WebSocketVO fromWebSocketVO = new WebSocketVO();
        BeanUtils.copyProperties(user, fromWebSocketVO);
        messageVO.setFormUser(fromWebSocketVO);
        messageVO.setText(text);
        messageVO.setTeamId(team.getId());
        messageVO.setChatType(chatType);
        messageVO.setCreateTime(DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss"));
        // 如果用户是团队领导或管理员，则设置为管理员
        if (user.getId()==team.getUserId() || user.getUserRole() == ADMIN_ROLE) {
            messageVO.setIsAdmin(true);
        }
        // 检查消息发送者是否为登录用户
        User loginUser = (User) this.httpSession.getAttribute(USER_LOGIN_STATE);
        if (loginUser.getId()==user.getId()) {
            messageVO.setIsMy(true);
        }
        // 将消息对象转换为JSON并广播到团队
        String toJson = new Gson().toJson(messageVO);
        try {
            broadcast(String.valueOf(team.getId()), toJson);
            saveChat(user.getId(), null, text, team.getId(), chatType);
            // 更新后删除团队的聊天缓存
            chatService.deleteKey(CACHE_CHAT_TEAM, String.valueOf(team.getId()));
            log.info("团队聊天，发送者={}, 团队={}, 在线用户数={}", user.getId(), team.getId(), getOnlineCount());
        } catch (Exception e) {
            log.error("处理团队聊天时发生错误", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 处理大厅聊天消息，将消息发送给所有在线用户，并保存聊天记录到数据库。
     * @param user     发送消息的用户。
     * @param text     要发送的消息文本。
     * @param chatType 聊天类型（例如：大厅聊天）。
     */
    private void hallChat(User user, String text, Integer chatType) {
        MessageVO messageVO = new MessageVO();
        WebSocketVO fromWebSocketVO = new WebSocketVO();
        BeanUtils.copyProperties(user, fromWebSocketVO);
        messageVO.setFormUser(fromWebSocketVO);
        messageVO.setText(text);
        messageVO.setChatType(chatType);
        messageVO.setCreateTime(DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss"));
        // 如果用户是管理员，则标记为管理员消息
        if (user.getUserRole() == ADMIN_ROLE) {
            messageVO.setIsAdmin(true);
        }
        // 如果发送消息的用户是当前登录用户，则标记消息为“我的消息”
        User loginUser = (User) this.httpSession.getAttribute(USER_LOGIN_STATE);
        if (loginUser.getId()==user.getId()) {
            messageVO.setIsMy(true);
        }
        // 转换消息为JSON格式并广播到所有在线用户
        String toJson = new Gson().toJson(messageVO);
        sendAllMessage(toJson);
        // 保存聊天记录并清除相关缓存
        saveChat(user.getId(), null, text, null, chatType);
        chatService.deleteKey(CACHE_CHAT_HALL, String.valueOf(user.getId()));
    }

    /**
     * 向指定团队的所有成员广播消息。
     * @param teamId 团队的唯一标识ID。
     * @param msg 广播的消息内容。
     * @throws Exception 如果发送消息过程中发生错误。
     */
    public static void broadcast(String teamId, String msg) {
        // 获取指定团队ID的WebSocket映射。
        ConcurrentHashMap<String, WebSocket> map = ROOMS.get(teamId);
        if (map != null) {
            // 遍历当前团队的所有成员（WebSocket连接）并发送消息。
            for (String key : map.keySet()) {
                try {
                    WebSocket webSocket = map.get(key);
                    webSocket.sendMessage(msg);  // 调用实例方法发送消息。
                } catch (Exception e) {
                    e.printStackTrace();  // 打印异常信息，通常应替换为更合适的错误处理。
                }
            }
        }
    }

    /**
     * 向当前WebSocket会话发送消息。
     * @param message 要发送的文本消息。
     * @throws IOException 如果在发送消息时发生输入输出异常。
     */
    public void sendMessage(String message) throws IOException {
        // 使用WebSocket的session对象发送文本消息。
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 向指定用户发送错误消息。
     * @param userId 接收错误消息的用户ID
     * @param errorMessage 错误信息内容
     */
    private void sendError(String userId, String errorMessage) {
        JSONObject obj = new JSONObject();
        obj.set("error", errorMessage);
        sendOneMessage(userId, obj.toString());
    }

    /**
     * 保存聊天记录到数据库。
     * @param userId 发送消息的用户ID。
     * @param toId 接收消息的用户ID（私聊时有效，团队聊天可为null）。
     * @param text 消息内容。
     * @param teamId 团队ID（团队聊天时有效，私聊可为null）。
     * @param chatType 聊天类型（如私聊、团队聊天等）。
     */
    private void saveChat(Long userId, Long toId, String text, Long teamId, Integer chatType) {
        // 对私聊进行特殊处理，确保双方是好友关系
        if (chatType == PRIVATE_CHAT) {
            User user = userService.getById(userId);  // 获取发送者的用户信息
            Set<Long> userIds = stringJsonListToLongSet(user.getUserIds());  // 获取用户的好友列表
            // 如果接收者不在发送者的好友列表中，则返回错误信息
            if (!userIds.contains(toId)) {
                sendError(String.valueOf(userId), "该用户不是你的好友");
                return;
            }
        }
        Chat chat = new Chat();  // 创建一个新的聊天记录对象
        chat.setFromId(userId);  // 设置消息的发送者ID
        chat.setText(text);  // 设置消息内容
        chat.setChatType(chatType);  // 设置聊天类型
        chat.setCreateTime(new Date());  // 设置消息的创建时间
        // 如果指定了接收者ID且有效，则设置接收者ID
        if (toId != null && toId > 0) {
            chat.setToId(toId);
        }
        // 如果指定了团队ID且有效，则设置团队ID
        if (teamId != null && teamId > 0) {
            chat.setTeamId(teamId);
        }
        chatService.save(chat);  // 调用聊天服务保存聊天记录到数据库
    }

    /**
     * 广播消息到所有连接的客户端
     * @param message 要广播的消息内容
     */
    public void sendAllMessage(String message) {
        log.info("【WebSocket消息】广播消息：" + message);
        for (Session session : SESSIONS) {
            try {
                if (session.isOpen()) {
                    synchronized (session) {
                        session.getBasicRemote().sendText(message);  // 使用同步方式发送消息，确保消息顺序发送
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送消息给指定的单个用户
     * @param userId  接收用户的ID
     * @param message 要发送的消息内容
     */
    public void sendOneMessage(String userId, String message) {
        Session session = SESSION_POOL.get(userId);  // 从会话池中获取用户会话
        if (session != null && session.isOpen()) {
            try {
                synchronized (session) {
                    log.info("【WebSocket消息】单点消息：" + message);
                    session.getAsyncRemote().sendText(message);  // 异步发送消息
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 广播所有在线用户的信息
     * 此方法构建了所有在线用户的列表，并将其格式化为JSON后广播
     */
    public void sendAllUsers() {
        log.info("【WebSocket消息】发送所有在线用户信息");
        HashMap<String, List<WebSocketVO>> stringListHashMap = new HashMap<>();
        List<WebSocketVO> WebSocketVOs = new ArrayList<>();
        stringListHashMap.put("users", WebSocketVOs);
        for (Serializable key : SESSION_POOL.keySet()) {
            User user = userService.getById(key);  // 从用户服务获取用户信息
            WebSocketVO WebSocketVO = new WebSocketVO();
            BeanUtils.copyProperties(user, WebSocketVO);  // 复制用户信息到WebSocket视图对象
            WebSocketVOs.add(WebSocketVO);
        }
        sendAllMessage(JSONUtil.toJsonStr(stringListHashMap));  // 将用户信息列表转换为JSON字符串并广播
    }

    // 获取当前在线的WebSocket连接数。
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    // 当新用户连接时，增加在线人数。
    public static synchronized void addOnlineCount() {
        WebSocket.onlineCount++;
    }

    // 当用户断开连接时，减少在线人数。
    public static synchronized void subOnlineCount() {
        WebSocket.onlineCount--;
    }

}
